In [ ]:
%%HTML
<style>
.container { width:100% }
</style>
The picture above shows an instance of the $3 \times 3$ sliding puzzle: There is a board of size $3 \times 3$ with 8 tiles on it. These tiles are numbered with digits from the set $\{1,\cdots, 8\}$. As the the $3 \times 3$ board has an area of $9$ but there are only $8$ tiles, there is an empty square on the board. Tiles adjacent to the empty square can be moved into the square, thereby emptying the space that was previously occupied by theses tiles. The goal of the $3 \times 3$ puzzle is to transform the state shown on the left of the picture above into the state shown on the right. We represent states as tuples of tuples. For example, the state shown above on the left side is represented as the following tuple:
( (8, 0, 6),
(5, 4, 7),
(2, 3, 1)
)
If we represent states this way, given a state s
, the expression s[row][col]
returns the tile in the specified row
and col
.
In order to get an idea of the sliding puzzle, you can play it online at http://mypuzzle.org/sliding.
The function call find_tile(tile, State)
finds the coordinates of the given tile
in State
. The tile
is represented as a number from the set $\{0,1,\cdots,8\}$, where $0$ represents the empty tile. The parameter State
is a tuple of tuples that specifies the positions of the tiles. If (row, col)
is the result returned by find_tile
, then we have:
State[row][col] == tile
In [ ]:
def find_tile(tile, State):
n = len(State)
for row in range(n):
for col in range(n):
if State[row][col] == tile:
return row, col
Since breadth first search stores the set of states that have been visited, we have to represent states by immutable objects and hence we represent the states as tuples of tuples. In order to be able to change these states, we have to transform these tuples of tuples into lists of lists.
The function to_list
transforms a tuple of tuples into a list of lists.
In [ ]:
to_list = lambda State: [list(row) for row in State]
The function to_tuple
transforms a list of lists into a tuple of tuples.
In [ ]:
to_tuple = lambda State: tuple(tuple(row) for row in State)
Given a State
that satisfies
State[row][col] == 0
and a direction (dx, dy)
that is an element form the set
$\bigl\{ (1, 0), (-1, 0), (0, 1), (0, -1) \bigr\}$,
the function move_dir
moves the empty tile in the direction (dx, dy)
.
In [ ]:
def move_dir(State, row, col, dx, dy):
State = to_list(State)
State[row ][col ] = State[row + dx][col + dy]
State[row + dx][col + dy] = 0
return to_tuple(State)
Given a State
of the sliding puzzle, the function next_states(State)
computes all those states that can be reached from State
in one step.
In [ ]:
def next_states(State):
n = len(State)
row, col = find_tile(0, State)
New_States = set()
Directions = [ (1, 0), (-1, 0), (0, 1), (0, -1) ]
for dx, dy in Directions:
if row + dx in range(n) and col + dy in range(n):
New_States.add(move_dir(State, row, col, dx, dy))
return New_States
Below, we have defined the start state, which is the state shown in the figure above on the left.
In [ ]:
start = ( (8, 0, 6),
(5, 4, 7),
(2, 3, 1)
)
In [ ]:
next_states(start)
In [ ]:
goal = ( (0, 1, 2),
(3, 4, 5),
(6, 7, 8)
)
Below is an instance of the $4 \times 4$ puzzle that can be solved in 40 steps.
In [ ]:
start2 = ( ( 0, 1, 2, 3 ),
( 4, 5, 6, 8 ),
( 14, 7, 11, 10 ),
( 9, 15, 12, 13 )
)
goal2 = ( ( 0, 1, 2, 3 ),
( 4, 5, 6, 7 ),
( 8, 9, 10, 11 ),
( 12, 13, 14, 15 )
)
For informed search we need to implement a
heuristic that estimates to distance between two states.
The function manhattan
implemented below takes as argument two states of the sliding puzzle and computes the Manhattan distance between these states.
Basically, the manhattan distance measure the number of moves that it would take to transform stateA
into stateB
if we were allowed to slide different tiles on top of each other.
In [ ]:
def manhattan(stateA, stateB):
n = len(stateA)
result = 0
for rowA in range(n):
for colA in range(n):
tile = stateA[rowA][colA]
if tile != 0:
rowB, colB = find_tile(tile, stateB)
result += abs(rowA - rowB) + abs(colA - colB)
return result
In [ ]:
manhattan(start, goal)
The package ipycanvas
, which is imported below, can be installed using the following command:
conda install -c conda-forge ipycanvas
This package is useful for drawings and animations. Its documentation can be found at: https://ipycanvas.readthedocs.io/en/latest/.
In [ ]:
import ipycanvas as cnv
The module time
is part of the standard library so it is preinstalled. We have imported it because we need the function time.sleep(secs)
to pause the animation for a specified time.
In [ ]:
import time
The global variable Colors
specifies the colors of the tiles.
In [ ]:
Colors = ['white', 'lightblue', 'pink', 'magenta', 'orange', 'red', 'yellow', 'lightgreen', 'gold',
'CornFlowerBlue', 'Coral', 'Cyan', 'orchid', 'DarkSalmon', 'DeepPink', 'green'
]
The global variable size
specifies the size of one tile in pixels.
In [ ]:
size = 100
The function draw(State, canvas, dx, dy, tile, x)
draws a given State
of the sliding puzzle, where tile
has been moved by offset
pixels into the direction (dx, dy)
.
In [ ]:
def draw(State, canvas, dx, dy, tile, offset):
canvas.text_align = 'center'
canvas.text_baseline = 'middle'
with cnv.hold_canvas(canvas):
canvas.clear()
n = len(State)
for row in range(n):
for col in range(n):
tile_to_draw = State[row][col]
color = Colors[tile_to_draw]
canvas.fill_style = color
if tile_to_draw not in (0, tile):
x = col * size
y = row * size
canvas.fill_rect(x, y, size, size)
canvas.line_width = 3.0
x += size // 2
y += size // 2
canvas.stroke_text(str(tile_to_draw), x, y)
elif tile_to_draw == tile:
x = col * size + offset * dx
y = row * size + offset * dy
canvas.fill_rect(x, y, size, size)
canvas.line_width = 3.0
x += size // 2
y += size // 2
if tile_to_draw != 0:
canvas.stroke_text(str(tile_to_draw), x, y)
In [ ]:
def create_canvas(n):
canvas = cnv.Canvas(size=(size * n, size * n))
canvas.font = '100px serif'
return canvas
The global variable delay
controls the speed of the animation.
In [ ]:
delay = 0.002
The function call tile_and_direction(state, next_state)
takes a state and the state that follows this state and returns a triple (tile, dx, dy)
where tile
is the tile that is moved to transform state
into next_state
and (dx, dy)
is the direction in which this tile is moved.
In [ ]:
def tile_and_direction(state, next_state):
row0, col0 = find_tile(0, state)
row1, col1 = find_tile(0, next_state)
return state[row1][col1], col0-col1, row0-row1
Given a list of states representing a solution to the sliding puzzle, the function call
animation(Solution)
animates the solution.
In [ ]:
def animation(Solution):
start = Solution[0]
n = len(start)
canvas = create_canvas(n)
draw(start, canvas, 0, 0, 0, 0)
m = len(Solution)
display(canvas)
for i in range(m-1):
state = Solution[i]
tile, dx, dy = tile_and_direction(state, Solution[i+1])
for offset in range(size+1):
draw(state, canvas, dx, dy, tile, offset)
time.sleep(delay)
In [ ]: